package cc.yaomm.common.basic.util;

import cc.yaomm.common.basic.enums.OSSDirectory;
import cc.yaomm.common.basic.excel.ExcelExportConfig;
import cc.yaomm.common.basic.excel.ExcelImportConfig;
import cc.yaomm.common.basic.exception.BusinessException;
import cc.yaomm.common.basic.file.FileService;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.exception.ExcelAnalysisException;
import com.alibaba.excel.exception.ExcelCommonException;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import org.apache.poi.openxml4j.exceptions.OLE2NotOfficeXmlFileException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;

/**
 * <p>@Description : Excel导入导出工具类 </p>
 * <p>@Author : QiLin.Xing </p>
 * <p>@Date : 2021/10/20 10:58 </p>
 */
public final class ExcelUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(ExcelUtils.class);

    private static final int EXCEL_ROW_LIMIT = 1000000; //Excel行数限制  1024*1024

    private static FileService fileService;

    private static ThreadPoolTaskExecutor taskExecutor;

    static {
        taskExecutor = SpringContextUtils.getBean(AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME, ThreadPoolTaskExecutor.class);
        if (Objects.isNull(taskExecutor)) {
            throw new NoSuchBeanDefinitionException("没有在Spring容器中查到ThreadPoolTaskExecutor bean对象");
        }

        fileService = SpringContextUtils.getBean(FileService.class);
        if (Objects.isNull(fileService)) {
            throw new NoSuchBeanDefinitionException("没有在Spring容器中查到FileService bean对象");
        }
    }

    /**
     * <p>@Description : 常规模型化导出</p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2019/11/6 0006 上午 9:08 </p>
     *
     * @param config 导出参数
     * @return java.lang.String
     */
    public static String exportExcel(ExcelExportConfig config) {
        //校验参数
        Assert.hasText(config.getFileName(), "导出失败，参数设置错误，getFileName不能为空");
        Assert.isTrue(config.getPageSize() > 0, "导出失败，参数设置错误，getPageSize必须设置>0");
        Assert.isTrue(config.getModelClass() != null
                || !CollectionUtils.isEmpty(config.getHeads()), "导出失败，参数设置错误，getModelClass不能为空或者getHeads不能为空");

        LOGGER.debug("开始导出[{}]Excel", config.getFileName());

        //判断totalCount是否为零，如果为零表示只导一次
        int count = config.getTotalCount();
        Assert.isTrue(count >= 0, "导出失败，参数设置错误，getTotalCount必须设置>=0");
        int totalPage = count > 0 ? count / config.getPageSize() + (count % config.getPageSize() > 0 ? 1 : 0) : 1;
        LOGGER.info("导出[{}]，总数据[{}]，总页数[{}]", config.getFileName(), count, totalPage);

        //获取线程池
        List<Future<List>> futures = new ArrayList<>(totalPage);
        Stream.iterate(1, x -> x + 1).limit(totalPage).forEach(x -> {
            futures.add(taskExecutor.submit(() -> config.getData(x, config.getPageSize())));
        });
        Assert.isTrue(!CollectionUtils.isEmpty(futures), "导出失败，没有查询任何数据");

        //region 写入Excel文件
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        ExcelWriterBuilder writerBuilder = EasyExcel.write(output);
        //判断是否排除导出列
        if (!CollectionUtils.isEmpty(config.getExcludeColumns())) {
            writerBuilder.excludeColumnFiledNames(config.getExcludeColumns());
        }

        //判断是否指定导出列
        if (!CollectionUtils.isEmpty(config.getIncludeColumns())) {
            writerBuilder.includeColumnFiledNames(config.getIncludeColumns());
        }

        //判断是否动态头导出
        if (!CollectionUtils.isEmpty(config.getHeads())) {
            writerBuilder.head(config.getHeads());
        } else {
            writerBuilder.head(config.getModelClass());
        }

        //判断是否有合并策略
        if (config.getLoopMergeStrategy() != null) {
            writerBuilder.registerWriteHandler(config.getLoopMergeStrategy());
        }

        ExcelWriter writer = writerBuilder.build();
        WriteSheet writeSheet = EasyExcel.writerSheet(0, config.getFileName()).build();

        int futureSize = futures.size();
        for (int i = 1; i <= futureSize; i++) {
            Future<List> future = futures.get(i - 1);
            try {
                List dataList = future.get();
                writer.write(dataList, writeSheet);
            } catch (Exception e) {
                LOGGER.error("写入[{}]第[{}]页数据到Excel中时异常", config.getFileName(), i, e);
            }
            int sheetNo = i * config.getPageSize() / EXCEL_ROW_LIMIT;
            if (writeSheet.getSheetNo() < sheetNo) {
                writeSheet = EasyExcel.writerSheet(sheetNo, config.getFileName() + "_" + sheetNo).build();
            }
        }

        writer.finish();
        try {
            output.close();
            //非中断的情况下才生成excel文件
            return fileService.upload(config.getFileName(), output.toByteArray(), OSSDirectory.TEMP, false);
        } catch (Exception e) {
            LOGGER.error("导出[{}]Excel时，关闭文件流异常", config.getFileName(), e);
        }
        LOGGER.debug("结束写线程[{}]", config.getFileName());

        //endregion
        return null;
    }

    /**
     * <p>@Description : 支持多sheet的excel导出 <br>
     * 注意:ExcelExportConfig中的getFileName是作为Excel表格的sheet名称，并且不能重复。
     * </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2019/11/6 0006 上午 9:08 </p>
     *
     * @param fileName Excel文件名
     * @param configs  导出参数
     * @return java.lang.String
     */
    public static String exportExcel(String fileName, ExcelExportConfig... configs) {
        //校验是否有配置参数
        Assert.notEmpty(configs, "导出失败，请设置导出参数，configs不能为空");

        //校验参数
        Assert.hasText(fileName, "导出失败，请设置Excel的文件名，fileName不能为空");

        //循环验证参数
        for (ExcelExportConfig config : configs) {
            Assert.hasText(config.getFileName(), "导出失败，参数设置错误，getFileName不能为空");
            Assert.isTrue(config.getPageSize() > 0, "导出失败，参数设置错误，getPageSize必须设置>0");
            Assert.isTrue(config.getModelClass() != null
                    || !CollectionUtils.isEmpty(config.getHeads()), "导出失败，参数设置错误，getModelClass不能为空或者getHeads不能为空");
        }

        LOGGER.debug("开始导出[{}]Excel", fileName);

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        ExcelWriter writer = EasyExcel.write(output).build();

        AtomicReference<Integer> sheetNo = new AtomicReference<>(0);
        AtomicReference<Integer> tmpSheetNo = new AtomicReference<>(0);
        //循环任务
        for (int index = 0; index < configs.length; index++) {
            ExcelExportConfig config = configs[index];
            LOGGER.debug("开始导出[{}]Excel-Sheet[{}]", fileName, config.getFileName());

            //判断totalCount是否为零，如果为零表示只导一次
            int count = config.getTotalCount();
            Assert.isTrue(count >= 0, "导出失败，参数设置错误，getTotalCount必须设置>=0");
            int totalPage = count > 0 ? count / config.getPageSize() + (count % config.getPageSize() > 0 ? 1 : 0) : 1;
            LOGGER.info("导出[{}]，总数据[{}]，总页数[{}]", config.getFileName(), count, totalPage);

            //获取线程池
            List<Future<List>> futures = new ArrayList<>(totalPage);
            Stream.iterate(1, x -> x + 1).limit(totalPage).forEach(x -> {
                futures.add(taskExecutor.submit(() -> config.getData(x, config.getPageSize())));
            });
            Assert.isTrue(!CollectionUtils.isEmpty(futures), "导出失败，没有查询任何数据");


            ExcelWriterSheetBuilder sheetBuilder = EasyExcel.writerSheet(sheetNo.get(), config.getFileName());
            //WriteSheet writeSheet = EasyExcel.writerSheet(config.getFileName()).build();

            //判断是否排除导出列
            if (!CollectionUtils.isEmpty(config.getExcludeColumns())) {
                sheetBuilder.excludeColumnFiledNames(config.getExcludeColumns());
            }

            //判断是否指定导出列
            if (!CollectionUtils.isEmpty(config.getIncludeColumns())) {
                sheetBuilder.includeColumnFiledNames(config.getIncludeColumns());
            }

            //判断是否动态头导出
            if (!CollectionUtils.isEmpty(config.getHeads())) {
                sheetBuilder.head(config.getHeads());
            } else {
                sheetBuilder.head(config.getModelClass());
            }

            //判断是否有合并策略
            if (config.getLoopMergeStrategy() != null) {
                sheetBuilder.registerWriteHandler(config.getLoopMergeStrategy());
            }

            WriteSheet writeSheet = sheetBuilder.build();

            int futureSize = futures.size();
            for (int i = 1; i <= futureSize; i++) {
                Future<List> future = futures.get(i - 1);
                try {
                    List dataList = future.get();
                    writer.write(dataList, writeSheet);
                } catch (Exception e) {
                    LOGGER.error("写入[{}]第[{}]页数据到Excel中时异常", config.getFileName(), i, e);
                }
                int oneSheetNo = i * config.getPageSize() / EXCEL_ROW_LIMIT;
                if (writeSheet.getSheetNo() - tmpSheetNo.get() < oneSheetNo) {
                    sheetNo.getAndSet(sheetNo.get() + 1);
                    sheetBuilder.sheetNo(sheetNo.get());
                    sheetBuilder.sheetName(config.getFileName() + "_" + (writeSheet.getSheetNo() - tmpSheetNo.get()));
                    writeSheet = sheetBuilder.build();
                }
            }

            sheetNo.getAndSet(sheetNo.get() + 1);
            tmpSheetNo.getAndSet(sheetNo.get());
            futures.clear();
        }

        writer.finish();
        try {
            output.close();
            return fileService.upload(fileName, output.toByteArray(), OSSDirectory.TEMP, false);
        } catch (IOException e) {
            LOGGER.error("导出[{}]Excel时，关闭文件流异常", fileName, e);
        }

        LOGGER.debug("结束导出[{}]Excel", fileName);
        return null;
    }

    /**
     * <p>@Description : 基于模型导入 </p>
     * <p>@Author : QiLin.Xing </p>
     * <p>@Date : 2021/7/21 14:46 </p>
     */
    public static void importExcel(ExcelImportConfig config) throws BusinessException {
        MultipartFile file = config.getFile();
        Assert.notNull(file, "Excel文件不能为空");

        //验证文件大小
        Assert.isTrue(file.getSize() < config.getFileSize(), "Excel文件超过文件大小限制");

        //验证文件格式
        String suffix = StringUtils.getFilenameExtension(file.getOriginalFilename());
        List<String> fileFormat = config.getFileFormat();
        Assert.isTrue(fileFormat.stream().anyMatch(suffix::equalsIgnoreCase), "Excel文件格式不支持");

        //读取Excel文件
        try {
            ExcelReaderBuilder read = EasyExcel.read(config.getFile().getInputStream(), config.getModelClass(), config.getListener());
            List<Converter> converts = config.getConverts();
            if (!CollectionUtils.isEmpty(converts)) {
                converts.stream().forEach(x -> read.registerConverter(x));
            }
            read.sheet().headRowNumber(config.getHeadRowNum()).doRead();
        } catch (Exception e) {
            LOGGER.error("导入异常", e);
            String msg = "导入失败，请稍候重试";
            if (e instanceof OLE2NotOfficeXmlFileException) {
                msg = "导入失败，请检查文件格式是否正确";
            } else if (e instanceof ExcelAnalysisException) {
                msg = "导入失败，解析Excel文件异常，请稍候重试";
                if (e.getCause() instanceof ExcelCommonException) {
                    msg = e.getCause().getLocalizedMessage();
                }
            } else if (e instanceof ExcelCommonException) {
                msg = e.getLocalizedMessage();
            }
            throw new BusinessException(msg);
        }
    }

}
